﻿//-----------------------------------------------------------------------
// <copyright file="FxCopRunner.cs">
//     Copyright (c) Nicole Calinoiu. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;

using Microsoft.FxCop.Sdk;

using Bordecal.FxCop.Sdk.Testing.AppDomainCustomization;

namespace Bordecal.FxCop.Sdk.Testing
{
	/// <summary>
	/// Helper class used to run FxCop and compare analysis results.
	/// </summary>
	public sealed class FxCopRunner
	{
		#region Constructors

		/// <summary>
		/// Initializes a new instance of the <see cref="FxCopRunner"/> class.
		/// </summary>
		/// <param name="fxCopCmdPath">The path to fxcopcmd.exe.</param>
		/// <param name="ruleFile">The path to the rule assembly file.</param>
		/// <param name="targetFiles">The path(s) to the analysis target file(s).</param>
		/// <param name="expectedResultsFiles">A list of paths to the FxCop report files containing expected rule violations.</param>
		/// <param name="assertFailMethod">The method to be invoked when failing a violation matching assertion.</param>
		public FxCopRunner(
				string fxCopCmdPath,
				string ruleFile,
				IEnumerable<string> targetFiles,
				IEnumerable<string> expectedResultsFiles,
				Action<string> assertFailMethod)
			: this(fxCopCmdPath, new string[] { ruleFile }, targetFiles, null, expectedResultsFiles, assertFailMethod)
		{
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="FxCopRunner"/> class.
		/// </summary>
		/// <param name="fxCopCmdPath">The path to fxcopcmd.exe.</param>
		/// <param name="ruleFiles">The path(s) to the rule assembly file(s).</param>
		/// <param name="targetFiles">The path(s) to the analysis target file(s).</param>
		/// <param name="ruleConfigurations">Instances of configurable rules for which non-default settings should be used.  A <c>null</c> value may be supplied to indicate that no settings overrides are to be applied.</param>
		/// <param name="expectedResultsFiles">A list of paths to the FxCop report files containing expected rule violations.</param>
		/// <param name="assertFailMethod">The method to be invoked when failing a violation matching assertion.</param>
		public FxCopRunner(
				string fxCopCmdPath,
				IEnumerable<string> ruleFiles,
				IEnumerable<string> targetFiles,
				IEnumerable<IConfigurableRule> ruleConfigurations,
				IEnumerable<string> expectedResultsFiles,
				Action<string> assertFailMethod)
		{
			if (string.IsNullOrEmpty(fxCopCmdPath))
			{
				throw new ArgumentNullException("fxCopCmdPath");
			}

			if (targetFiles == null)
			{
				throw new ArgumentNullException("targetFiles");
			}

			if (expectedResultsFiles == null)
			{
				throw new ArgumentNullException("expectedResultsFiles");
			}

			if (assertFailMethod == null)
			{
				throw new ArgumentNullException("assertFailMethod");
			}

			this.ExpectedIssues = FxCopRunner.ExtractExpectedIssues(expectedResultsFiles, targetFiles);
			this.AssertFailMethod = assertFailMethod;

			string projectPath = FxCopProjectFile.Create(ruleFiles, targetFiles, ruleConfigurations);
			this.RunAnalysis(fxCopCmdPath, projectPath);

			File.Delete(projectPath);
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="FxCopRunner"/> class.
		/// </summary>
		/// <param name="fxCopCmdPath">The path to fxcopcmd.exe.</param>
		/// <param name="projectPath">The path to the FxCop project that should be analyzed.</param>
		/// <param name="expectedResultsFiles">A list of paths to the FxCop report files containing expected rule violations.</param>
		/// <param name="assertFailMethod">The method to be invoked when failing a violation matching assertion.</param>
		public FxCopRunner(
			string fxCopCmdPath,
			string projectPath,
			IEnumerable<string> expectedResultsFiles,
			Action<string> assertFailMethod)
		{
			if (string.IsNullOrEmpty(fxCopCmdPath))
			{
				throw new ArgumentNullException("fxCopCmdPath");
			}

			if (string.IsNullOrEmpty(projectPath))
			{
				throw new ArgumentNullException("projectPath");
			}

			if (expectedResultsFiles == null)
			{
				throw new ArgumentNullException("expectedResultsFiles");
			}

			if (assertFailMethod == null)
			{
				throw new ArgumentNullException("assertFailMethod");
			}

			this.ExpectedIssues = FxCopRunner.ExtractExpectedIssues(expectedResultsFiles, new ProjectFile(projectPath).TargetPaths);
			this.RunAnalysis(fxCopCmdPath, projectPath);
			this.AssertFailMethod = assertFailMethod;
		}

		#endregion

		#region Properties

		private bool IsAnalysisComplete { get; set; }

		private IList<Issue> ActualIssues { get; set; }

		private IList<Issue> ExpectedIssues { get; set; }

		private Action<string> AssertFailMethod { get; set; }

		#endregion

		#region Methods

		private static void RunAnalysisInProcess(string fxCopCmdPath, string projectPath, string outputPath)
		{
			ProcessStartInfo startInfo = new ProcessStartInfo(fxCopCmdPath);
			startInfo.Arguments = string.Format(
				CultureInfo.InvariantCulture,
				@"/project:""{0}"" /out:""{1}""",
				projectPath,
				outputPath);
			startInfo.CreateNoWindow = true;
			startInfo.UseShellExecute = false;
			startInfo.RedirectStandardError = true;
			startInfo.RedirectStandardOutput = true;

			using (Process process = Process.Start(startInfo))
			{
				string error = process.StandardError.ReadToEnd();
				string output = process.StandardOutput.ReadToEnd();

				process.WaitForExit();

				if (process.ExitCode != 0)
				{
					throw new OperationCanceledException(error + "\r\n" + output);
				}
			}
		}

		private static IEnumerable<Issue> GetRuleIssues(IList<Issue> allIssues, string checkId)
		{
			return from issue in allIssues
				   where issue.Message.CheckId == checkId
				   select issue;
		}

		private static void RecordMismatches(StringBuilder builder, string checkId, IEnumerable<Issue> expectedIssues, IEnumerable<Issue> actualIssues, string reportPattern)
		{
			if (builder.Length > 0)
			{
				builder.AppendLine();
			}

			foreach (Issue issue in actualIssues.Except(expectedIssues, IssueEqualityComparer.Default))
			{
				if (builder.Length > 0)
				{
					builder.AppendLine();
				}

				builder.AppendFormat(
					CultureInfo.CurrentCulture,
					reportPattern,
					checkId,
					issue.File,
					issue.Line,
					issue.Text);
			}
		}

		private static IList<Issue> ExtractExpectedIssues(IEnumerable<string> expectedResultsFiles, IEnumerable<string> targetFiles)
		{
			return ReportReader.ExtractIssues(expectedResultsFiles)
					.Concat(new ExpectedViolationAttributeReader().GetIssues(targetFiles))
					.ToList();
		}

		private void RunAnalysis(string fxCopCmdPath, string projectPath)
		{
			string outputPath = Path.GetTempFileName();

			if (Debugger.IsAttached)
			{
				this.RunAnalysisInAppDomain(fxCopCmdPath, projectPath, outputPath);
			}
			else
			{
				FxCopRunner.RunAnalysisInProcess(fxCopCmdPath, projectPath, outputPath);
			}

			this.ActualIssues = ReportReader.ExtractIssues(outputPath);
			File.Delete(outputPath);

			this.IsAnalysisComplete = true;
		}

		[SuppressMessage("Microsoft.StyleCop.CSharp.ReadabilityRules", "SA1118:ParameterMustNotSpanMultipleLines",
			Justification = "False positive.")]
		private void RunAnalysisInAppDomain(string fxCopCmdPath, string projectPath, string outputPath)
		{
			AppDomainSetup setup = new AppDomainSetup();
			setup.ApplicationBase = Path.GetDirectoryName(this.GetType().Assembly.Location);

			AppDomain appDomain = AppDomain.CreateDomain("FxCopRunner", null, setup);
			try
			{
				using (ConsoleWriter writer = new ConsoleWriter())
				{
					writer.CustomizeAppDomain(appDomain);
					new ThreadNameClearer().CustomizeAppDomain(appDomain);

					setup = new AppDomainSetup();
					setup.ApplicationBase = Path.GetDirectoryName(fxCopCmdPath);
					setup.ConfigurationFile = fxCopCmdPath + ".config";
					new FusionStoreSetter(setup).CustomizeAppDomain(appDomain);

					int result = appDomain.ExecuteAssemblyByName(
						Path.GetFileNameWithoutExtension(fxCopCmdPath),
						string.Format(CultureInfo.InvariantCulture, @"/project:""{0}""", projectPath),
						string.Format(CultureInfo.InvariantCulture, @"/out:""{0}""", outputPath));

					if (result != 0)
					{
						throw new OperationCanceledException(writer.ToString());
					}
				}
			}
			finally
			{
				AppDomain.Unload(appDomain);
			}
		}

		/// <summary>
		/// Makes a test assertion that verifies whether the actual results of the
		/// FxCop run match the expected violation set.
		/// </summary>
		/// <remarks>
		/// Both of the following must be true for the assertion to pass:
		///	<list type="number">
		///		<item>
		///			<description>Every violation in the expected list must have a match in the actual list.</description>
		///		</item>
		///		<item>
		///			<description>Every violation in the actual list must have a match in the expected list.</description>
		///		</item>
		/// </list>
		/// </remarks>
		/// <param name="checkId">The CheckId of the FxCop rule for which violations should be matched.</param>
		public void AssertActualViolationsMatchExpectedViolations(string checkId)
		{
			if (string.IsNullOrEmpty(checkId))
			{
				throw new ArgumentNullException(checkId);
			}

			if (!this.IsAnalysisComplete)
			{
				throw new InvalidOperationException(Strings.FxCopRunNotComplete);
			}

			IEnumerable<Issue> expectedIssues = FxCopRunner.GetRuleIssues(this.ExpectedIssues, checkId);
			IEnumerable<Issue> actualIssues = FxCopRunner.GetRuleIssues(this.ActualIssues, checkId);

			StringBuilder builder = new StringBuilder();

			FxCopRunner.RecordMismatches(builder, checkId, actualIssues, expectedIssues, Strings.ExpectedViolationNotFound);
			FxCopRunner.RecordMismatches(builder, checkId, expectedIssues, actualIssues, Strings.UnexpectedViolationFound);

			if (builder.Length > 0)
			{
				 this.AssertFailMethod(builder.ToString());
			}
		}

		#endregion
	}
}